Skip to main content

copp\copp\copp3\opt3/
copp3_socp.rs

1//! 3rd-order Convex-Objective Path Parameterization (COPP3) based on second-order cone programming (SOCP).
2//!
3//! # Method identity
4//! This module implements the **optimization backend** for COPP3 by transforming
5//! third-order path-parameterization constraints/objectives into Clarabel-compatible
6//! conic form and solving with SOCP.
7//!
8//! # Discrete variables (local notation)
9//! On a path grid `s[0..=n]`:
10//! - `a[k]` denotes $\dot{s}_k^2$;
11//! - `b[k]` denotes $\ddot{s}_k$;
12//! - decision vector starts with `x = [a[0..=n], b[0..=n], x_others]`, where
13//!   `x_others` are auxiliary variables introduced by objectives ([`Time`](crate::prelude::CoppObjective::Time),
14//!   [`ThermalEnergy`](crate::prelude::CoppObjective::ThermalEnergy), [`TotalVariationTorque`](crate::prelude::CoppObjective::TotalVariationTorque), [`Linear`](crate::prelude::CoppObjective::Linear)).
15//!
16//! # High-level pipeline
17//! 1. Validate interval/boundary/objective contract.
18//! 2. Assemble standard TOPP3 conic constraints.
19//! 3. Add COPP3 objective-induced variables/cones.
20//! 4. Build sparse matrices `A`, `P`, vector `q`, and solve by Clarabel.
21//! 5. Apply status acceptance policy ([`ClarabelOptions::is_allow`](crate::solver::copp2_socp::ClarabelOptions::is_allow)) and extract
22//!    `(a,b)` only when accepted.
23//!
24//! # API layering
25//! - [`copp3_socp`](crate::solver::copp3_socp::copp3_socp): strict/normal API, returns only accepted [`Topp3Profile`](crate::solver::copp3_socp::Topp3Profile).
26//! - [`copp3_socp_expert`](crate::solver::copp3_socp::copp3_socp_expert): expert API returning `(Option<Topp3Profile>, DefaultSolution<f64>)`.
27//! - [`copp3_socp_expert_with_info`](crate::solver::copp3_socp::copp3_socp_expert_with_info): expert API plus Clarabel linear-solver
28//!   metadata for wrappers that need solver-side diagnostics.
29
30use crate::copp::clarabel_backend::{ConstraintsClarabel, ObjConsClarabel};
31use crate::copp::copp3::Topp3Profile;
32use crate::copp::copp3::Topp3ProfileRef;
33use crate::copp::copp3::formulation::{Copp3Problem, get_weight_a_copp3, get_weight_a_topp3};
34use crate::copp::copp3::opt3::ClarabelExpertInfor3rd;
35use crate::copp::copp3::opt3::clarabel_constraints::{
36    clarabel_standard_capacity_topp3, clarabel_standard_constraint_topp3,
37};
38use crate::copp::{
39    ClarabelOptions, CoppObjective, clarabel_to_copp3_solution, validate_copp3_objectives,
40};
41use crate::diag::{
42    CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
43    check_boundary_state_copp3_valid, check_s_interval_valid, format_duration_human,
44};
45use crate::robot::robot_core::{Robot, RobotBasic, RobotTorque};
46use clarabel::algebra::CscMatrix;
47use clarabel::solver::SupportedConeT::{NonnegativeConeT, SecondOrderConeT};
48use clarabel::solver::{DefaultSolution, DefaultSolver, IPSolver, SupportedConeT};
49use core::f64;
50use itertools::{Itertools, izip};
51use nalgebra::{DMatrix, DVectorView};
52
53/// Strict COPP3-SOCP API for production use.
54///
55/// # Purpose
56/// Use this entry when caller only needs a valid [`Topp3Profile`](crate::solver::copp3_socp::Topp3Profile) and treats
57/// non-accepted solver statuses as hard failures.
58///
59/// # Contract
60/// - Internally calls [`copp3_socp_expert`](crate::solver::copp3_socp::copp3_socp_expert).
61/// - Returns `Ok(Topp3Profile { .. })` **iff** `options.is_allow(solution.status)` is `true`.
62/// - Returns [`Err(CoppError::ClarabelSolverStatus(...))`](CoppError::ClarabelSolverStatus) when status is not accepted.
63///
64/// # Returns
65/// Returns accepted COPP3 profile.
66///
67/// # Errors
68/// Returns [`CoppError`](crate::diag::CoppError) on model/solver failures and non-accepted solver status.
69///
70/// # Notes
71/// For workflows requiring low-level diagnostics (`status` and raw Clarabel solution fields),
72/// prefer [`copp3_socp_expert`](crate::solver::copp3_socp::copp3_socp_expert).
73pub fn copp3_socp<'a, M: RobotTorque>(
74    problem: &Copp3Problem<'a, M>,
75    options: &ClarabelOptions,
76) -> Result<Topp3Profile, CoppError> {
77    let (result, solution) = copp3_socp_expert(problem, options)?;
78    result.ok_or_else(|| CoppError::ClarabelSolverStatus("copp3_socp".into(), solution.status))
79}
80
81/// Expert COPP3-SOCP API with full Clarabel solution exposure.
82///
83/// # Return contract
84/// - `Ok((Some(result), solution))`: status accepted by `options.is_allow(solution.status)`.
85/// - `Ok((None, solution))`: solve finished but status not accepted.
86/// - `Err(...)`: input/model/solver-construction runtime failures.
87///
88/// # Returns
89/// Returns tuple `(Option<Topp3Profile>, DefaultSolution<f64>)` for diagnostic use.
90///
91/// # Errors
92/// Returns [`CoppError`](crate::diag::CoppError) only for true runtime failures.
93///
94/// # Contract
95/// - caller must handle `None` profile for non-accepted statuses;
96/// - status acceptance policy is defined by `options.is_allow`.
97///
98/// # Verbosity behavior
99/// Logging is layered by `options.verbosity()`:
100/// - [`Silent`](Verbosity::Silent): no algorithm logs;
101/// - [`Summary`](Verbosity::Summary): lifecycle milestones and elapsed time;
102/// - [`Debug`](Verbosity::Debug): assembly-level counters and stage summaries;
103/// - [`Trace`](Verbosity::Trace): fine-grained stage deltas and solver snapshot diagnostics.
104pub fn copp3_socp_expert<'a, M: RobotTorque>(
105    problem: &Copp3Problem<'a, M>,
106    options: &ClarabelOptions,
107) -> Result<(Option<Topp3Profile>, DefaultSolution<f64>), CoppError> {
108    let info = copp3_socp_expert_with_info(problem, options)?;
109    let _ = &info.linsolver;
110    Ok((info.result, info.solution))
111}
112
113/// Expert COPP3-SOCP API with Clarabel solution and linear-solver diagnostics.
114///
115/// Use this variant when callers need more than
116/// [`DefaultSolution`](clarabel::solver::DefaultSolution), because Clarabel stores linear-solver metadata on the
117/// solver `info` object rather than inside the returned solution.
118pub fn copp3_socp_expert_with_info<'a, M: RobotTorque>(
119    problem: &Copp3Problem<'a, M>,
120    options: &ClarabelOptions,
121) -> Result<ClarabelExpertInfor3rd, CoppError> {
122    match options.verbosity() {
123        Verbosity::Silent => copp3_socp_core(problem, (options, SilentVerboser)),
124        Verbosity::Summary => copp3_socp_core(problem, (options, SummaryVerboser::new())),
125        Verbosity::Debug => copp3_socp_core(problem, (options, DebugVerboser::new())),
126        Verbosity::Trace => copp3_socp_core(problem, (options, TraceVerboser::new())),
127    }
128}
129
130/// Core implementation for COPP3-SOCP expert flow.
131///
132/// # Internal contract
133/// `options_verboser` packs:
134/// - `options`: acceptance policy and Clarabel numerical settings;
135/// - `verboser`: concrete logger implementation chosen by external verbosity dispatch.
136///
137/// # Invariants
138/// - decision-variable layout always starts with contiguous `a[0..=n]` and `b[0..=n]`;
139/// - `q_object.len()` is treated as final `n_var` before solver build;
140/// - extracted `(a,b)` is produced only through [`clarabel_to_copp3_solution`](crate::solver::copp3_socp::clarabel_to_copp3_solution) when status is accepted.
141fn copp3_socp_core<'a, M: RobotTorque>(
142    problem: &Copp3Problem<'a, M>,
143    options_verboser: (&ClarabelOptions, impl Verboser),
144) -> Result<ClarabelExpertInfor3rd, CoppError> {
145    let (options, mut verboser) = options_verboser;
146    let idx_s_start = problem.idx_s_start;
147    let a_boundary = problem.a_boundary;
148    let b_boundary = problem.b_boundary;
149    let num_stationary = problem.num_stationary;
150    if verboser.is_enabled(Verbosity::Summary) {
151        verboser.record_start_time();
152    }
153    if verboser.is_enabled(Verbosity::Trace) {
154        let settings = options.clarabel_settings();
155        crate::verbosity_log!(
156            crate::diag::Verbosity::Summary,
157            "copp3_socp: options snapshot -> allow(almost={}, max_iter={}, max_time={}, callback_term={}, insufficient_progress={}), tol_gap_rel={}, tol_feas={}, max_iter={}, verbose={}",
158            options.is_allow(clarabel::solver::SolverStatus::AlmostSolved),
159            options.is_allow(clarabel::solver::SolverStatus::MaxIterations),
160            options.is_allow(clarabel::solver::SolverStatus::MaxTime),
161            options.is_allow(clarabel::solver::SolverStatus::CallbackTerminated),
162            options.is_allow(clarabel::solver::SolverStatus::InsufficientProgress),
163            settings.tol_gap_rel,
164            settings.tol_feas,
165            settings.max_iter,
166            settings.verbose
167        );
168    }
169    // Check input validity
170    check_boundary_state_copp3_valid(a_boundary, b_boundary)?;
171    let n = problem.a_linearization.len() - 1;
172    let idx_s_final = idx_s_start + n;
173    if verboser.is_enabled(Verbosity::Summary) {
174        crate::verbosity_log!(
175            crate::diag::Verbosity::Summary,
176            "\ncopp3_socp started: {} <= idx_s <= {}, objectives = {}, s_len = {}.",
177            idx_s_start,
178            idx_s_final,
179            problem.objectives.len(),
180            problem.a_linearization.len()
181        );
182    }
183    check_s_interval_valid("copp3_socp", idx_s_start, idx_s_final)?;
184    validate_copp3_objectives(
185        "copp3_socp",
186        problem.objectives,
187        problem.robot.dim(),
188        problem.a_linearization.len(),
189    )?;
190    // Let x = [a[0,1,...,n],
191    //          b[0,1,...,n],
192    //          xi[0,1,...,len_xi-1], (if xi exists.)
193    //          x_others] \in R^{2*(n+1), x_others}.
194    // Step 1. Deal with constraints
195    // Step 1.1 Compute the number of constraints
196    let (cap_val_std, cap_b_std, cap_cone_std) =
197        clarabel_standard_capacity_topp3(&problem.robot.constraints, (idx_s_start, idx_s_final));
198    let (cap_val_obj, cap_b_obj, cap_cone_obj, n_vars) =
199        clarabel_objective_capacity_copp3(n, problem.objectives, problem.robot);
200    if verboser.is_enabled(Verbosity::Debug) {
201        crate::verbosity_log!(
202            crate::diag::Verbosity::Summary,
203            "copp3_socp: capacity estimate std(val={cap_val_std}, b={cap_b_std}, cone={cap_cone_std}), obj(val={cap_val_obj}, b={cap_b_obj}, cone={cap_cone_obj}), n_vars={n_vars}."
204        );
205    }
206    // s=b-A*x \in cone, where A[row[i],col[i]]=val[i], A \in R^{m*(n+1)}, b \in R^m, s \in R^m
207    // -s=-b+A*x
208    let mut row = Vec::<usize>::with_capacity(cap_val_std + cap_val_obj);
209    let mut col = Vec::<usize>::with_capacity(cap_val_std + cap_val_obj);
210    let mut val = Vec::<f64>::with_capacity(cap_val_std + cap_val_obj);
211    let mut b = Vec::<f64>::with_capacity(cap_b_std + cap_b_obj);
212    let mut cones = Vec::<SupportedConeT<f64>>::with_capacity(cap_cone_std + cap_cone_obj);
213    if verboser.is_enabled(Verbosity::Trace) {
214        crate::verbosity_log!(
215            crate::diag::Verbosity::Summary,
216            "copp3_socp: allocated capacities row/col/val/b/cones <= {}/{}/{}/{}/{}",
217            cap_val_std + cap_val_obj,
218            cap_val_std + cap_val_obj,
219            cap_val_std + cap_val_obj,
220            cap_b_std + cap_b_obj,
221            cap_cone_std + cap_cone_obj
222        );
223    }
224    // Step 1.2 set constraints of the standard topp3-lp problem
225    let s = problem
226        .robot
227        .constraints
228        .s_vec(idx_s_start, idx_s_final + 1)?;
229    let row_before_std = row.len();
230    let col_before_std = col.len();
231    let val_before_std = val.len();
232    let b_before_std = b.len();
233    let cones_before_std = cones.len();
234    clarabel_standard_constraint_topp3(
235        &problem.as_topp3_problem(),
236        &s,
237        (&mut row, &mut col, &mut val, &mut b, &mut cones),
238        num_stationary,
239        &verboser,
240    )?;
241    if verboser.is_enabled(Verbosity::Trace) {
242        crate::verbosity_log!(
243            crate::diag::Verbosity::Summary,
244            "copp3_socp: standard-constraints delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}",
245            row.len() - row_before_std,
246            col.len() - col_before_std,
247            val.len() - val_before_std,
248            b.len() - b_before_std,
249            cones.len() - cones_before_std
250        );
251    }
252    // Step 2. set objective
253    // Step 2.1. determine whether xi=sqrt(a) is needed.
254    let row_before_sqrt = row.len();
255    let col_before_sqrt = col.len();
256    let val_before_sqrt = val.len();
257    let b_before_sqrt = b.len();
258    let cones_before_sqrt = cones.len();
259    let n_var_old = clarabel_sqrt_a_copp3(
260        n,
261        problem.objectives,
262        (&mut row, &mut col, &mut val, &mut b, &mut cones),
263        num_stationary,
264    );
265    if verboser.is_enabled(Verbosity::Trace) {
266        crate::verbosity_log!(
267            crate::diag::Verbosity::Summary,
268            "copp3_socp: sqrt-a stage delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}, n_var_old={}",
269            row.len() - row_before_sqrt,
270            col.len() - col_before_sqrt,
271            val.len() - val_before_sqrt,
272            b.len() - b_before_sqrt,
273            cones.len() - cones_before_sqrt,
274            n_var_old
275        );
276    }
277    let mut q_object = Vec::<f64>::with_capacity(n_vars);
278    q_object.resize(n_var_old, 0.0);
279    // Step 2.2. add constraints and objective for each term in the objective.
280    let row_before_obj = row.len();
281    let col_before_obj = col.len();
282    let val_before_obj = val.len();
283    let b_before_obj = b.len();
284    let cones_before_obj = cones.len();
285    let q_before_obj = q_object.len();
286    clarabel_objective_copp3(
287        problem,
288        num_stationary,
289        (
290            &mut row,
291            &mut col,
292            &mut val,
293            &mut b,
294            &mut cones,
295            &mut q_object,
296        ),
297    )?;
298    if verboser.is_enabled(Verbosity::Trace) {
299        let (q_min, q_max) = q_object
300            .iter()
301            .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), &v| {
302                (mn.min(v), mx.max(v))
303            });
304        crate::verbosity_log!(
305            crate::diag::Verbosity::Summary,
306            "copp3_socp: objective stage delta row/col/val/b/cones/q = +{}/+{}/+{}/+{}/+{}/+{}, q_range=[{}, {}]",
307            row.len() - row_before_obj,
308            col.len() - col_before_obj,
309            val.len() - val_before_obj,
310            b.len() - b_before_obj,
311            cones.len() - cones_before_obj,
312            q_object.len() - q_before_obj,
313            q_min,
314            q_max
315        );
316    }
317    if verboser.is_enabled(Verbosity::Debug) {
318        crate::verbosity_log!(
319            crate::diag::Verbosity::Summary,
320            "copp3_socp: after objective assembly row={}, col={}, val={}, b={}, cones={}, q={}",
321            row.len(),
322            col.len(),
323            val.len(),
324            b.len(),
325            cones.len(),
326            q_object.len()
327        );
328    }
329    // Step 2.3 build the constraints
330    let n_var = q_object.len();
331    let row_len = row.len();
332    let col_len = col.len();
333    let val_len = val.len();
334    let b_len = b.len();
335    let cones_len = cones.len();
336    let a_csc = CscMatrix::new_from_triplets(b.len(), n_var, row, col, val);
337    let p_object = CscMatrix::<f64>::zeros((n_var, n_var));
338    if verboser.is_enabled(Verbosity::Trace) {
339        crate::verbosity_log!(
340            crate::diag::Verbosity::Summary,
341            "copp3_socp: matrix built with m={}, n={}, A.nnz={}, P.nnz={}",
342            b.len(),
343            n_var,
344            a_csc.nnz(),
345            p_object.nnz()
346        );
347    }
348    if verboser.is_enabled(Verbosity::Summary) {
349        crate::verbosity_log!(
350            crate::diag::Verbosity::Summary,
351            "copp3_socp: ready to solve with row/col/val/b/cones = {row_len}/{col_len}/{val_len}/{b_len}/{cones_len} and n_var = {n_var}.",
352        );
353    }
354    // Step 3. solve the SOCP problem
355    let settings = options.clarabel_settings().clone();
356    let mut solver = DefaultSolver::<f64>::new(&p_object, &q_object, &a_csc, &b, &cones, settings)
357        .map_err(|e| CoppError::ClarabelSolverError("copp3_socp".into(), e))?;
358    solver.solve();
359    let linsolver = solver.info.linsolver.clone();
360    let solution = solver.solution;
361    if verboser.is_enabled(Verbosity::Summary) {
362        crate::verbosity_log!(
363            crate::diag::Verbosity::Summary,
364            "copp3_socp: solve done, status = {:?}, elapsed = {}.",
365            solution.status,
366            format_duration_human(verboser.elapsed())
367        );
368    }
369    if verboser.is_enabled(Verbosity::Trace) {
370        let show = solution.x.len().min(3);
371        crate::verbosity_log!(
372            crate::diag::Verbosity::Summary,
373            "copp3_socp: solution x_len={}, head={:?}",
374            solution.x.len(),
375            &solution.x[0..show]
376        );
377    }
378    let result = if options.is_allow(solution.status) {
379        Some(clarabel_to_copp3_solution(
380            &solution.x.as_slice()[0..2 * (n + 1)],
381            &s,
382            num_stationary,
383        ))
384    } else {
385        None
386    };
387    if verboser.is_enabled(Verbosity::Trace) {
388        crate::verbosity_log!(
389            crate::diag::Verbosity::Summary,
390            "copp3_socp: allow(status)={}, extracted_profile={}",
391            options.is_allow(solution.status),
392            if result.is_some() {
393                "Some(Topp3Profile)"
394            } else {
395                "None"
396            }
397        );
398    }
399    Ok(ClarabelExpertInfor3rd {
400        result,
401        solution,
402        linsolver,
403    })
404}
405
406/// Determine the length of xi[k] = sqrt(a[k + k_skip]) in the decision variable x.
407#[inline(always)]
408fn length_xi(n: usize, num_stationary: (usize, usize)) -> usize {
409    n + 1 - num_stationary.0.max(1) - num_stationary.1.max(1)
410}
411
412/// Return k_skip, where xi[k] = sqrt(a[k + k_skip])
413#[inline(always)]
414fn skip_a_for_xi(num_stationary_start: usize) -> usize {
415    num_stationary_start.max(1)
416}
417
418/// Add the constraints for sqrt(a) >= xi in COPP3 optimization.
419/// x = [a[0,...,n], b[0,...,n], xi[0,...,len_xi-1], ...] \in R^{2*(n+1)+len_xi+...}.
420/// sqrt(a[k]) >= xi[k] >= 0
421/// num_val <= 4*n, num_b <= 4*n, num_cones <= n
422/// Return the len of the new x: n+1 or 2*(n+1)
423fn clarabel_sqrt_a_copp3(
424    n: usize,
425    objective: &[CoppObjective],
426    constraints: ConstraintsClarabel,
427    num_stationary: (usize, usize),
428) -> usize {
429    let (row, col, val, b, cones) = constraints;
430    for obj in objective {
431        match obj {
432            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _) => {
433                let n_skip = skip_a_for_xi(num_stationary.0);
434                let len_xi = length_xi(n, num_stationary); // < n
435                // xi >= 0
436                // A*x-b = -s = -1*xi[k] <= 0
437                row.extend(b.len()..b.len() + len_xi);
438                col.extend((2 * (n + 1))..(2 * (n + 1) + len_xi));
439                val.resize(val.len() + len_xi, -1.0);
440                b.resize(b.len() + len_xi, 0.0);
441                cones.push(NonnegativeConeT(len_xi));
442                // sqrt(a) >= xi
443                // xi^2 <= a
444                // xi^2 + (a - 0.25)^2 <= (a + 0.25)^2
445                // [a[k]+0.25, a[k]-0.25, xi[k]] \in SOC
446                // -A*x+b = s = [x[k+n_skip]+0.25, x[k+n_skip]-0.25, x[2*(n+1)+k]] \in SOC
447                row.extend(b.len()..b.len() + 3 * len_xi);
448                val.resize(val.len() + 3 * len_xi, -1.0);
449                cones.resize(cones.len() + len_xi, SecondOrderConeT(3));
450                for k in 0..len_xi {
451                    col.extend([k + n_skip, k + n_skip, 2 * (n + 1) + k]);
452                    b.extend([0.25, -0.25, 0.0]);
453                }
454                return 2 * (n + 1) + len_xi;
455            }
456            _ => {}
457        }
458    }
459    2 * (n + 1)
460}
461
462/// Determine the number of clarabel's capacity for the objective in COPP3.
463fn clarabel_objective_capacity_copp3<M: RobotBasic>(
464    n: usize,
465    objective: &[CoppObjective],
466    robot: &Robot<M>,
467) -> (usize, usize, usize, usize) {
468    // Step 1. sqrt(a[k]) >= xi[k] >= 0
469    // num_val <= 4*n, num_b <= 4*n, num_cones <= n, n_var <= n
470    let (mut capacity_val, mut capacity_b, mut capacity_cones, mut n_vars) =
471        if objective.iter().any(|obj| {
472            matches!(
473                obj,
474                CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _)
475            )
476        }) {
477            (4 * n, 4 * n, n, 2 * (n + 1))
478        } else {
479            (0, 0, 0, 2 * (n + 1))
480        };
481    // Step 2. objective function
482    let dim = robot.dim();
483    for obj in objective {
484        match obj {
485            CoppObjective::Time(_) => {
486                // num_val <= 5*n, num_b <= 4*n, num_cones <= n, n_var <= n
487                capacity_val += 5 * n;
488                capacity_b += 4 * n;
489                capacity_cones += n;
490                n_vars += n;
491            }
492            CoppObjective::ThermalEnergy(_, _) => {
493                // num_val <= (4+2*dim)*(n+1), num_b <= (2+dim)*(n+1), num_cones <= n+1, n_var <= n+1
494                capacity_val += (4 + 2 * dim) * (n + 1);
495                capacity_b += (dim + 2) * (n + 1);
496                capacity_cones += n + 1;
497                n_vars += n + 1;
498            }
499            CoppObjective::TotalVariationTorque(_, _) => {
500                // num_val <= 10*n*dim, num_b <= 2*n*dim, num_cones <= 1, n_var <= n*dim
501                capacity_val += 10 * dim * n;
502                capacity_b += 2 * dim * n;
503                capacity_cones += 1;
504                n_vars += dim * n;
505            }
506            _ => {}
507        }
508    }
509    (capacity_val, capacity_b, capacity_cones, n_vars)
510}
511
512fn clarabel_objective_copp3<M: RobotTorque>(
513    problem: &Copp3Problem<M>,
514    num_stationary: (usize, usize),
515    objective_constraints: ObjConsClarabel,
516) -> Result<(), CoppError> {
517    let (row, col, val, b, cones, q_object) = objective_constraints;
518    let n = problem.a_linearization.len() - 1;
519    let s = problem
520        .robot
521        .constraints
522        .s_vec(problem.idx_s_start, problem.idx_s_start + n + 1)?;
523    let weight_a_time = if problem
524        .objectives
525        .iter()
526        .any(|obj| matches!(obj, CoppObjective::Time(_)))
527    {
528        get_weight_a_topp3(&s, num_stationary)
529    } else {
530        vec![]
531    };
532    let weight_a_torque = if problem
533        .objectives
534        .iter()
535        .any(|obj| matches!(obj, CoppObjective::ThermalEnergy(_, _)))
536    {
537        get_weight_a_copp3(&s, num_stationary)
538    } else {
539        vec![]
540    };
541    let coeffs_torque = if problem.objectives.iter().any(|obj| {
542        matches!(
543            obj,
544            CoppObjective::ThermalEnergy(_, _) | CoppObjective::TotalVariationTorque(_, _)
545        )
546    }) {
547        // shape: (dim, n) since there are n+1 a and n b.
548        problem.robot.torque_coeff(problem.idx_s_start, n + 1)?
549    } else {
550        (
551            DMatrix::<f64>::zeros(0, 0),
552            DMatrix::<f64>::zeros(0, 0),
553            DMatrix::<f64>::zeros(0, 0),
554        )
555    };
556    for obj in problem.objectives {
557        match obj {
558            CoppObjective::Time(weight) => {
559                if !clarabel_objective_time_copp3(
560                    &s,
561                    *weight,
562                    &weight_a_time,
563                    num_stationary,
564                    (row, col, val, b, cones, q_object),
565                ) {
566                    return Err(CoppError::InvalidInput(
567                        "clarabel_objective_copp3".into(),
568                        "Invalid Time objective".into(),
569                    ));
570                }
571            }
572            CoppObjective::ThermalEnergy(weight, normalize) => {
573                if !clarabel_objective_thermal_energy_copp3(
574                    &weight_a_torque,
575                    *weight,
576                    normalize,
577                    &coeffs_torque,
578                    num_stationary,
579                    (row, col, val, b, cones, q_object),
580                ) {
581                    return Err(CoppError::InvalidInput(
582                        "clarabel_objective_copp3".into(),
583                        "Invalid ThermalEnergy objective".into(),
584                    ));
585                }
586            }
587            CoppObjective::TotalVariationTorque(weight, normalize) => {
588                if !clarabel_objective_tv_torque_copp3(
589                    *weight,
590                    normalize,
591                    &coeffs_torque,
592                    num_stationary,
593                    (row, col, val, b, cones, q_object),
594                ) {
595                    return Err(CoppError::InvalidInput(
596                        "clarabel_objective_copp3".into(),
597                        "Invalid TotalVariationTorque objective".into(),
598                    ));
599                }
600            }
601            CoppObjective::Linear(weight, alpha, beta) => {
602                if !clarabel_objective_linear_copp3(
603                    &s,
604                    *weight,
605                    alpha,
606                    beta,
607                    q_object,
608                    num_stationary,
609                ) {
610                    return Err(CoppError::InvalidInput(
611                        "clarabel_objective_copp3".into(),
612                        "Invalid Linear objective".into(),
613                    ));
614                }
615            }
616        }
617    }
618    Ok(())
619}
620
621/// Add the constraints and objective for Time in COPP3 optimization.
622/// num_val <= 5*n, num_b <= 4*n, num_cones <= n, n_var <= n
623fn clarabel_objective_time_copp3(
624    s: &[f64],
625    weight: f64,
626    weight_a: &[f64],
627    num_stationary: (usize, usize),
628    objective_constraints: ObjConsClarabel,
629) -> bool {
630    if weight < 0.0 {
631        return false;
632    }
633
634    let (row, col, val, b, cones, q_object) = objective_constraints;
635    let n = s.len() - 1;
636    let len_xi = length_xi(n, num_stationary);
637    let k_skip = skip_a_for_xi(num_stationary.0);
638    // Add constraints for xi and eta, where xi[k]=sqrt(a[k+k_skip]), eta[k]=1/xi[k]
639    // eta[k] >= 0,
640    // norm2([2, xi[k] - eta[k]]) <= xi[k] + eta[k]
641    let id_xi_start = 2 * (n + 1);
642    let id_eta_start = q_object.len();
643    // Step 1. eta[k] >= 0
644    // A*x-b = -s = -1*eta[k] = -1*x[id_eta_start + k] <= 0
645    row.extend(b.len()..(b.len() + len_xi));
646    col.extend(id_eta_start..(id_eta_start + len_xi));
647    val.resize(val.len() + len_xi, -1.0);
648    b.resize(b.len() + len_xi, 0.0);
649    cones.push(NonnegativeConeT(len_xi));
650    // Step 2. norm2([2, xi[k] - eta[k]]) <= xi[k] + eta[k]
651    // -A*x+b = s = [xi[k] + eta[k], xi[k] - eta[k], 2] \in SOC
652    for k in 0..len_xi {
653        // -A*x+b = s = [x[id_xi_start + k] + x[id_eta_start + k], x[id_xi_start + k] - x[id_eta_start + k], 2] \in SOC
654        col.extend([
655            id_xi_start + k,
656            id_eta_start + k,
657            id_xi_start + k,
658            id_eta_start + k,
659        ]);
660        row.extend([b.len(), b.len(), b.len() + 1, b.len() + 1]);
661        val.extend([-1.0, -1.0, -1.0, 1.0]);
662        b.extend([0.0, 0.0, 2.0]);
663    }
664    cones.resize(cones.len() + len_xi, SecondOrderConeT(3));
665    // Minimize sum[k in 0..len_xi] {weight_a[k+k_skip] / sqrt(a[k+k_skip])}
666    // Minimize sum[k in 0..len_xi] {weight_a[k+k_skip] * eta[k]}
667    q_object.extend(
668        weight_a
669            .iter()
670            .skip(k_skip)
671            .take(len_xi)
672            .map(|w| weight * w),
673    );
674
675    true
676}
677
678/// Add the constraints and objective for ThermalEnergy in COPP3 optimization.
679/// num_val <= (4+2*dim)*(n+1), num_b <= (2+dim)*(n+1), num_cones <= n+1, n_var <= n+1
680fn clarabel_objective_thermal_energy_copp3(
681    weight_a: &[f64],
682    weight: f64,
683    normalize: &[f64],
684    coeffs_torque: &(DMatrix<f64>, DMatrix<f64>, DMatrix<f64>),
685    num_stationary: (usize, usize),
686    objective_constraints: ObjConsClarabel,
687) -> bool {
688    if weight < 0.0 {
689        return false;
690    }
691    let (row, col, val, b, cones, q_object) = objective_constraints;
692    // minimize: weight * \int_{s[0]}^{s[n]} {\sum[i] {(tau[i][s] * normalize[i])^2 / sqrt(a[s])} ds}
693    let mut coeff_a = coeffs_torque.0.clone();
694    let mut coeff_b = coeffs_torque.1.clone();
695    let mut coeff_g = coeffs_torque.2.clone();
696    let dim = coeff_a.nrows();
697    if normalize.len() != dim {
698        return false;
699    }
700    let n = coeff_a.ncols() - 1;
701    // tau[i][k] = coeff_a[i][k] * a[k] + coeff_b[i][k] * b[k] + coeff_g[i][k]
702    let normalize = DVectorView::from_slice(normalize, dim);
703    for mut col in coeff_a.column_iter_mut() {
704        col.component_mul_assign(&normalize);
705    }
706    for mut col in coeff_b.column_iter_mut() {
707        col.component_mul_assign(&normalize);
708    }
709    for mut col in coeff_g.column_iter_mut() {
710        col.component_mul_assign(&normalize);
711    }
712    // tau[i][k] * normalize[i] = coeff_a[i][k] * a[k] + coeff_b[i][k] * b[k] + coeff_g[i][k]
713    let k_skip = skip_a_for_xi(num_stationary.0);
714    let len_xi = length_xi(n, num_stationary);
715
716    if num_stationary.0 > 0 {
717        // minimize: weight * \int_{s[0]}^{s[num_stationary.0]} {\sum[i] {tau_normal[i][s]^2 / sqrt(a[s])} ds}
718        // \approx weight * \sum[i] { tau_average[i]^2 * \int_{s[0]}^{s[num_stationary.0]} {1 / sqrt(a[s])} ds} }
719        // \int_{s[0]}^{s[num_stationary.0]} {1 / sqrt(a[s])} ds} = weight[0] / xi[0]
720        // minmize: weight * weight_a[0] * \sum[i] { tau_average[i]^2 / xi[0] }
721        // let tau_average[i] = (tau_normal[i][0] + tau_normal[i][num_stationary.0]) / 2
722        let col_a = coeff_a.column(num_stationary.0);
723        let col_b = coeff_b.column(num_stationary.0);
724        let col_g = coeff_g.column(num_stationary.0);
725        let col_g_0 = coeff_g.column(0);
726        // tau[0] = col_g_0
727        // tau[num_stationary.0] = col_a * a[num_stationary.0] + col_b * b[num_stationary.0] + col_g
728        // let \sum[i] { tau_average[i]^2 } <= t * xi[0]
729        // \sum[i](col_a[i] * a[num_stationary.0] + col_b[i] * b[num_stationary.0] + col_g[i] + col_g_0[i])^2 <= 4 * t
730        // -A*x+b = s = [t + xi[0], t - xi[0], -(col_a[i] * a[num_stationary.0] + col_b[i] * b[num_stationary.0] + col_g[i] + col_g_0[i])] \in SOC
731        let id_t = q_object.len();
732        // -A*x+b = [t + xi[0], t - xi[0]]
733        row.extend(b.len()..(b.len() + 2));
734        col.resize(col.len() + 2, id_t);
735        row.extend(b.len()..(b.len() + 2));
736        col.resize(col.len() + 2, 2 * (n + 1));
737        val.resize(val.len() + 3, -1.0);
738        val.push(1.0);
739        b.resize(b.len() + 2, 0.0);
740        // -A*x+b = [-(col_a[i] * a[num_stationary.0] + col_b[i] * b[num_stationary.0] + col_g[i] + col_g_0[i])] for i in 0..dim
741        row.extend((b.len())..(b.len() + dim));
742        col.resize(col.len() + dim, num_stationary.0);
743        val.extend(col_a.iter().take(dim));
744        row.extend((b.len())..(b.len() + dim));
745        col.resize(col.len() + dim, n + 1 + num_stationary.0);
746        val.extend(col_b.iter().take(dim));
747        b.extend(
748            col_g
749                .iter()
750                .zip(col_g_0.iter())
751                .take(dim)
752                .map(|(&g, &g_0)| -(g + g_0)),
753        );
754        cones.push(SecondOrderConeT(dim + 2));
755        q_object.push(weight * weight_a[0]);
756    }
757    if num_stationary.1 > 0 {
758        // minimize: weight * \int_{s[n-num_stationary.1]}^{s[n]} {\sum[i] {tau_normal[i][s]^2 / sqrt(a[s])} ds}
759        // \approx weight * \sum[i] { tau_average[i]^2 * \int_{s[n-num_stationary.1]}^{s[n]} {1 / sqrt(a[s])} ds} }
760        // \int_{s[n-num_stationary.1]}^{s[n]} {1 / sqrt(a[s])} ds} = weight[n-num_stationary.1] / xi[len_xi-1]
761        // minmize: weight * weight[n] * \sum[i] { tau_average[i]^2 }
762        // let tau_average[i] = (tau_normal[i][n] + tau_normal[i][n-num_stationary.1]) / 2
763        let col_a = coeff_a.column(n - num_stationary.1);
764        let col_b = coeff_b.column(n - num_stationary.1);
765        let col_g = coeff_g.column(n - num_stationary.1);
766        let col_g_f = coeff_g.column(n);
767        // tau[f] = col_g_n
768        // tau[n-num_stationary.1] = col_a * a[n-num_stationary.1] + col_b * b[n-num_stationary.1] + col_g
769        // let \sum[i] { tau_average[i]^2 } <= t * xi[len_xi-1]
770        // \sum[i](col_a[i] * a[n-num_stationary.1] + col_b[i] * b[n-num_stationary.1] + col_g[i] + col_g_f[i])^2 <= 4 * t * xi[len_xi-1]
771        // -A*x+b = s = [t + xi[len_xi-1], t - xi[len_xi-1], -(col_a[i] * a[n-num_stationary.1] + col_b[i] * b[n-num_stationary.1] + col_g[i] + col_g_f[i])] \in SOC
772        let id_t = q_object.len();
773        // -A*x+b = [t+xi[len_xi-1], t-xi[len_xi-1]]
774        row.extend(b.len()..(b.len() + 2));
775        col.resize(col.len() + 2, id_t);
776        row.extend(b.len()..(b.len() + 2));
777        col.resize(col.len() + 2, 2 * n + len_xi + 1);
778        val.resize(val.len() + 3, -1.0);
779        val.push(1.0);
780        b.resize(b.len() + 2, 0.0);
781        // -A*x+b = [-(col_a[i] * a[n-num_stationary.1] + col_b[i] * b[n-num_stationary.1] + col_g[i] + col_g_f[i])] for i in 0..dim
782        row.extend((b.len())..(b.len() + dim));
783        col.resize(col.len() + dim, n - num_stationary.1);
784        val.extend(col_a.iter().take(dim));
785        row.extend((b.len())..(b.len() + dim));
786        col.resize(col.len() + dim, 2 * n + 1 - num_stationary.1);
787        val.extend(col_b.iter().take(dim));
788        b.extend(
789            col_g
790                .iter()
791                .zip(col_g_f.iter())
792                .take(dim)
793                .map(|(&g, &g_f)| -(g + g_f)),
794        );
795        cones.push(SecondOrderConeT(dim + 2));
796        q_object.push(weight * weight_a[n]);
797    }
798
799    // minimize: weight * \sum[k] { \int_{s[k]}^{s[k+1]} {\sum[i] {tau_normal[i][s]^2 / sqrt(a[s])} ds} }
800    // Decouple the integral
801    // minimize: weight * \sum[k] { \sum[i] {tau_normal[i][k]^2} / sqrt(a[k]) * 0.5 * (s[k+1]-s[k-1]) }
802    let id_t_start = q_object.len();
803    for (k, (col_a, col_b, col_g)) in izip!(
804        coeff_a.column_iter(),
805        coeff_b.column_iter(),
806        coeff_g.column_iter()
807    )
808    .skip(k_skip)
809    .take(len_xi)
810    .enumerate()
811    {
812        // \sum[i] {(col_a[i] * a[k+k_skip] + col_b[i] * b[k+k_skip] + col_g[i])^2} / xi[k] <= 4 * t[k]
813        // \sum[i] {(col_a[i] * a[k+k_skip] + col_b[i] * b[k+k_skip] + col_g[i])^2} <= 4 * t[k] * xi[k]
814        // -A*x+b = s = [t[k] + xi[k], t[k] - xi[k], -(col_a[i] * a[k+k_skip] + col_b[i] * b[k+k_skip] + col_g[i])] \in SOC
815        // -A*x+b = s = [t[k] + xi[k], t[k] - xi[k]]
816        row.extend(b.len()..(b.len() + 2));
817        col.resize(col.len() + 2, id_t_start + k);
818        row.extend(b.len()..(b.len() + 2));
819        col.resize(col.len() + 2, 2 * (n + 1) + k);
820        val.resize(val.len() + 3, -1.0);
821        val.push(1.0);
822        b.resize(b.len() + 2, 0.0);
823        // -A*x+b = s = [-(col_a[i] * a[k+k_skip] + col_b[i] * b[k+k_skip] + col_g[i])]
824        row.extend((b.len())..(b.len() + dim));
825        col.resize(col.len() + dim, k + k_skip);
826        val.extend(col_a.iter().take(dim));
827        row.extend((b.len())..(b.len() + dim));
828        col.resize(col.len() + dim, n + 1 + k + k_skip);
829        val.extend(col_b.iter().take(dim));
830        b.extend(col_g.iter().take(dim).map(|&g| -g));
831    }
832    cones.resize(cones.len() + len_xi, SecondOrderConeT(dim + 2));
833    // minimize: 2 * weight * \sum[k] { t[k] * (s[k+1]-s[k-1]) }
834    // if num_stationary == (0,0), then \sum[k in 1..n] { t[k] * (s[k+1]-s[k-1]) }
835    // if num_stationary == (n1>0,n2>0), then \sum[k in (n1+1)..(n-n2-1)] { t[k] * (s[k+1]-s[k-1]) } + t[n1] * (s[n1+1]-s[n1]) + t[n-n2] * (s[n-n2]-s[n-n2-1])
836    let weight_four = 4.0 * weight;
837    q_object.extend(
838        weight_a
839            .iter()
840            .skip(k_skip)
841            .take(len_xi)
842            .map(|w| weight_four * w),
843    );
844    true
845}
846
847/// Add the constraints and objective for TotalVariationTorque in COPP3 optimization.
848/// num_val <= 10*n*dim, num_b <= 2*n*dim, num_cones <= 1, n_var <= n*dim
849fn clarabel_objective_tv_torque_copp3(
850    weight: f64,
851    normalize: &[f64],
852    coeffs_torque: &(DMatrix<f64>, DMatrix<f64>, DMatrix<f64>),
853    num_stationary: (usize, usize),
854    objective_constraints: ObjConsClarabel,
855) -> bool {
856    if weight < 0.0 {
857        return false;
858    }
859    let (row, col, val, b, cones, q_object) = objective_constraints;
860    // minimize: weight * \sum |tau[i][k+1]-tau[i][k]| * normalize[i]
861    // Let: |tau[i][k+1]-tau[i][k]| * normalize[i] <= t[i][k]
862    let mut coeff_a = coeffs_torque.0.clone();
863    let mut coeff_b = coeffs_torque.1.clone();
864    let mut coeff_g = coeffs_torque.2.clone();
865    let dim = coeff_a.nrows();
866    if normalize.len() != dim {
867        return false;
868    }
869    let n = coeff_a.ncols() - 1;
870    // tau[i][k] = coeff_a[i][k] * a[k] + coeff_b[i][k] * b[k] + coeff_g[i][k]
871    let normalize = DVectorView::from_slice(normalize, dim);
872    for mut col in coeff_a.column_iter_mut() {
873        col.component_mul_assign(&normalize);
874    }
875    for mut col in coeff_b.column_iter_mut() {
876        col.component_mul_assign(&normalize);
877    }
878    for mut col in coeff_g.column_iter_mut() {
879        col.component_mul_assign(&normalize);
880    }
881    // tau[i][k] * normalize[i] = coeff_a[i][k] * a[k] + coeff_b[i][k] * b[k] + coeff_g[i][k]
882    // (tau[i][k+1] - tau[i][k]) * normalize[i] = coeff_a[i][k+1] * a[k+1] + coeff_b[i][k+1] * b[k+1] + coeff_g[i][k+1] - coeff_a[i][k] * a[k] - coeff_b[i][k] * b[k] - coeff_g[i][k]
883
884    let n_b_old = b.len();
885    // A*x-b = -s = -coeff_a[i][k] * x[k] + coeff_a[i][k+1] * x[k+1] - coeff_b[i][k] * x[n+k+1] + coeff_b[i][k+1] * x[n+k+2] + coeff_g[i][k+1] - coeff_g[i][k] - t[i][k] <= 0
886    // A*x-b = -s = -(-coeff_a[i][k] * x[k] + coeff_a[i][k+1] * x[k+1] - coeff_b[i][k] * x[n+k+1] + coeff_b[i][k+1] * x[n+k+2] + coeff_g[i][k+1] - coeff_g[i][k] - t[i][k]) <= 0
887    if num_stationary.0 > 0 {
888        // Consider (tau[i][num_stationary.0] - tau[i][0]) * normalize[i] = coeff_a[i][num_stationary.0] * a[num_stationary.0] + coeff_b[i][num_stationary.0] * b[num_stationary.0] + coeff_g[i][num_stationary.0] - coeff_g[i][0]
889        let col_a = coeff_a.column(num_stationary.0);
890        let col_b = coeff_b.column(num_stationary.0);
891        let col_g = coeff_g.column(num_stationary.0);
892        let col_g_0 = coeff_g.column(0);
893        let n_var_old = q_object.len();
894        for (i, (&v_a, &v_b, &v_g, &v_g_0)) in
895            izip!(col_a.iter(), col_b.iter(), col_g.iter(), col_g_0.iter())
896                .take(dim)
897                .enumerate()
898        {
899            // dtau_normal = v_a * a[num_stationary.0] + v_b * b[num_stationary.0] + v_g - v_g_0
900            // A*x-b = -s = v_a * a[num_stationary.0] + v_b * b[num_stationary.0] + v_g - v_g_0 - t[i] <= 0
901            row.resize(row.len() + 3, b.len());
902            col.extend([num_stationary.0, n + num_stationary.0 + 1, n_var_old + i]);
903            val.extend([v_a, v_b, -1.0]);
904            b.push(v_g - v_g_0);
905            // A*x-b = -s = -(v_a * a[num_stationary.0] + v_b * b[num_stationary.0] + v_g - v_g_0) - t[i] <= 0
906            row.resize(row.len() + 3, b.len());
907            col.extend([num_stationary.0, n + num_stationary.0 + 1, n_var_old + i]);
908            val.extend([-v_a, -v_b, -1.0]);
909            b.push(v_g_0 - v_g);
910        }
911        q_object.resize(q_object.len() + dim, weight);
912    }
913
914    if num_stationary.1 > 0 {
915        // Consider (tau[i][n-num_stationary.1] - tau[i][n]) * normalize[i] = coeff_a[i][n-num_stationary.1] * a[n-num_stationary.1] + coeff_b[i][n-num_stationary.1] * b[n-num_stationary.1] + coeff_g[i][n-num_stationary.1] - coeff_g[i][n]
916        let col_a = coeff_a.column(n - num_stationary.1);
917        let col_b = coeff_b.column(n - num_stationary.1);
918        let col_g = coeff_g.column(n - num_stationary.1);
919        let col_g_f = coeff_g.column(n);
920        let n_var_old = q_object.len();
921        for (i, (&v_a, &v_b, &v_g, &v_g_f)) in
922            izip!(col_a.iter(), col_b.iter(), col_g.iter(), col_g_f.iter())
923                .take(dim)
924                .enumerate()
925        {
926            // dtau_normal = v_a * a[n-num_stationary.1] + v_b * b[n-num_stationary.1] + v_g - v_g_0
927            // A*x-b = -s = v_a * a[n-num_stationary.1] + v_b * b[n-num_stationary.1] + v_g - v_g_0 - t[i] <= 0
928            row.resize(row.len() + 3, b.len());
929            col.extend([
930                n - num_stationary.1,
931                n + n - num_stationary.1 + 1,
932                n_var_old + i,
933            ]);
934            val.extend([v_a, v_b, -1.0]);
935            b.push(v_g - v_g_f);
936            // A*x-b = -s = -(v_a * a[n-num_stationary.1] + v_b * b[n-num_stationary.1] + v_g - v_g_f) - t[i] <= 0
937            row.resize(row.len() + 3, b.len());
938            col.extend([
939                n - num_stationary.1,
940                n + n - num_stationary.1 + 1,
941                n_var_old + i,
942            ]);
943            val.extend([-v_a, -v_b, -1.0]);
944            b.push(v_g_f - v_g);
945        }
946        q_object.resize(q_object.len() + dim, weight);
947    }
948
949    // Consider (tau[i][k+1] - tau[i][k]) * normalize[i] for k in num_stationary.0..(n-num_stationary.1)
950    for (k, ((col_a_curr, col_b_curr, col_g_curr), (col_a_next, col_b_next, col_g_next))) in izip!(
951        coeff_a.column_iter(),
952        coeff_b.column_iter(),
953        coeff_g.column_iter()
954    )
955    .tuple_windows()
956    .enumerate()
957    .skip(num_stationary.0)
958    .take(n - num_stationary.0 - num_stationary.1)
959    {
960        let n_var_old = q_object.len();
961        for (i, (&v_a_curr, &v_b_curr, &v_g_curr, &v_a_next, &v_b_next, &v_g_next)) in izip!(
962            col_a_curr.iter(),
963            col_b_curr.iter(),
964            col_g_curr.iter(),
965            col_a_next.iter(),
966            col_b_next.iter(),
967            col_g_next.iter()
968        )
969        .enumerate()
970        {
971            // dtau_normal[i] = -v_a_curr * a[k] + v_a_next * a[k+1] - v_b_curr * b[k] + v_b_next * b[k+1] + v_g_next- v_g_curr
972            // A*x-b = -s = -v_a_curr * a[k] + v_a_next * a[k+1] - v_b_curr * b[k] + v_b_next * b[k+1] + v_g_next - v_g_curr - t[i][k] <= 0
973            row.resize(row.len() + 5, b.len());
974            col.extend([k, k + 1, n + k + 1, n + k + 2, n_var_old + i]);
975            val.extend([-v_a_curr, v_a_next, -v_b_curr, v_b_next, -1.0]);
976            b.push(v_g_next - v_g_curr);
977            // A*x-b = -s = -(-v_a_curr * a[k] + v_a_next * a[k+1] - v_b_curr * b[k] + v_b_next * b[k+1] + v_g_next - v_g_curr) - t[i][k] <= 0
978            row.resize(row.len() + 5, b.len());
979            col.extend([k, k + 1, n + k + 1, n + k + 2, n_var_old + i]);
980            val.extend([v_a_curr, -v_a_next, v_b_curr, -v_b_next, -1.0]);
981            b.push(v_g_curr - v_g_next);
982        }
983        q_object.resize(q_object.len() + dim, weight);
984    }
985    cones.push(NonnegativeConeT(b.len() - n_b_old));
986
987    true
988}
989
990/// Add the constraints and objective for Linear in COPP3 optimization.
991fn clarabel_objective_linear_copp3(
992    s: &[f64],
993    weight: f64,
994    alpha: &[f64],
995    beta: &[f64],
996    q_object: &mut [f64],
997    num_stationary: (usize, usize),
998) -> bool {
999    if alpha.len() != s.len() || beta.len() != s.len() {
1000        return false;
1001    }
1002    let n = s.len() - 1;
1003    // objective: minimize weight * \sum (alpha[k]*a[k] + beta[k]*b[k])
1004    if num_stationary.0 > 1 {
1005        let &s_start = s.first().unwrap();
1006        let ds_start = s[num_stationary.0] - s_start;
1007        let q_n1 = &mut q_object[num_stationary.0];
1008        for (&s_k, &alpha_k, &beta_k) in izip!(s.iter(), alpha.iter(), beta.iter())
1009            .skip(1)
1010            .take(num_stationary.0 - 1)
1011        {
1012            let dsk_start = s_k - s_start;
1013            let gamma = dsk_start / ds_start;
1014            // a_k = a[num_stationary.0] * gamma;
1015            // b_k = a[num_stationary.0] * gamma / (1.5 * dsk_start);
1016            *q_n1 += weight * gamma * gamma.cbrt() * (alpha_k + beta_k / (1.5 * dsk_start));
1017        }
1018    }
1019    if num_stationary.1 > 1 {
1020        let &s_final = s.last().unwrap();
1021        let ds_final = s[n - num_stationary.1] - s_final;
1022        let q_n2 = &mut q_object[n - num_stationary.1];
1023        for (&s_k, &alpha_k, &beta_k) in izip!(s.iter(), alpha.iter(), beta.iter())
1024            .skip(1)
1025            .take(num_stationary.1 - 1)
1026        {
1027            let dsk_final = s_k - s_final;
1028            let gamma = dsk_final / ds_final;
1029            // a_k = a[n - num_stationary.1] * gamma;
1030            // b_k = a[n - num_stationary.1] * gamma / (1.5 * dsk_start);
1031            *q_n2 += weight * gamma * gamma.cbrt() * (alpha_k + beta_k / (1.5 * dsk_final));
1032        }
1033    }
1034    for (q_k, &alpha_k) in q_object
1035        .iter_mut()
1036        .zip(alpha.iter())
1037        .take(n + 1 - num_stationary.1)
1038        .skip(num_stationary.0)
1039    {
1040        *q_k += weight * alpha_k;
1041    }
1042    for (q_k, &beta_k) in q_object
1043        .iter_mut()
1044        .skip(n + 1)
1045        .zip(beta.iter())
1046        .take(n + 1 - num_stationary.1)
1047        .skip(num_stationary.0)
1048    {
1049        *q_k += weight * beta_k;
1050    }
1051
1052    true
1053}
1054
1055/// Compute the time value in COPP3 optimization.
1056/// Input: a_sqrt_down = 1 / sqrt(a)
1057#[inline(always)]
1058fn objective_value_time_copp3(
1059    a_sqrt_down: &[f64],
1060    weight_a: &[f64],
1061    num_stationary: (usize, usize),
1062) -> f64 {
1063    let n = a_sqrt_down.len() - 1;
1064    let k_skip = skip_a_for_xi(num_stationary.0);
1065    let len_xi = length_xi(n, num_stationary);
1066    // objective: minimize \sum  weight_a[k] / sqrt(a[k])
1067    let mut objective = 0.0;
1068    for (a_sqrt_down, weight_a) in a_sqrt_down
1069        .iter()
1070        .zip(weight_a.iter())
1071        .skip(k_skip)
1072        .take(len_xi)
1073    {
1074        objective += weight_a * a_sqrt_down;
1075    }
1076    objective
1077}
1078
1079/// Compute the thermal energy value in COPP3 optimization.
1080#[inline(always)]
1081fn objective_value_thermal_energy_copp3(
1082    a_sqrt_down: &[f64],
1083    weight_a: &[f64],
1084    num_stationary: (usize, usize),
1085    torque: &DMatrix<f64>,
1086    normalize: &[f64],
1087) -> f64 {
1088    let mut objective = 0.0;
1089    let n = a_sqrt_down.len() - 1;
1090    if num_stationary.0 > 0 {
1091        // minmize: weight_a[0] / sqrt(a[num_stationary.0]) * \sum[i] { (tau_average[i] * normalize[i])^2 }
1092        // tau_average[i] = (tau_normal[i][0] + tau_normal[i][num_stationary.0]) / 2
1093        let torque_n1 = torque.column(num_stationary.0);
1094        let torque_0 = torque.column(0);
1095        let weight = weight_a[0] * a_sqrt_down[num_stationary.0];
1096        for (tau_n1, tau_0, &normalize_i) in
1097            izip!(torque_n1.iter(), torque_0.iter(), normalize.iter())
1098        {
1099            let tau_average = 0.5 * normalize_i * (tau_n1 + tau_0);
1100            objective += weight * tau_average * tau_average;
1101        }
1102    }
1103    if num_stationary.1 > 0 {
1104        // minmize: weight_a[n] / sqrt(a[n-num_stationary.1]) * \sum[i] { (tau_average[i] * normalize[i])^2 }
1105        // tau_average[i] = (tau_normal[i][n] + tau_normal[i][n-num_stationary.1]) / 2
1106        let torque_n2 = torque.column(n - num_stationary.1);
1107        let torque_n = torque.column(n);
1108        let weight = weight_a[n] * a_sqrt_down[n - num_stationary.1];
1109        for (tau_n2, tau_n, &normalize_i) in
1110            izip!(torque_n2.iter(), torque_n.iter(), normalize.iter())
1111        {
1112            let tau_average = 0.5 * normalize_i * (tau_n2 + tau_n);
1113            objective += weight * tau_average * tau_average;
1114        }
1115    }
1116    let k_skip = skip_a_for_xi(num_stationary.0);
1117    let len_xi = length_xi(n, num_stationary);
1118    for (torque_k, &a_sqrt_down_k, &weight_a_k) in
1119        izip!(torque.column_iter(), a_sqrt_down.iter(), weight_a.iter())
1120            .skip(k_skip)
1121            .take(len_xi)
1122    {
1123        for (tau_k, &normalize_i) in torque_k.iter().zip(normalize.iter()) {
1124            let tau_normal = tau_k * normalize_i;
1125            objective += 4.0 * weight_a_k * tau_normal * tau_normal * a_sqrt_down_k;
1126        }
1127    }
1128
1129    objective
1130}
1131
1132/// Compute the thermal energy value in COPP3 optimization.
1133#[inline(always)]
1134fn objective_value_tv_torque_copp3(
1135    torque: &DMatrix<f64>,
1136    normalize: &[f64],
1137    num_stationary: (usize, usize),
1138) -> f64 {
1139    let mut objective = 0.0;
1140    let n = torque.ncols() - 1;
1141    if num_stationary.0 > 0 {
1142        // |tau[i][num_stationary.0] - tau[i][0]| * normalize[i]
1143        let torque_n1 = torque.column(num_stationary.0);
1144        let torque_0 = torque.column(0);
1145        for (tau_n1, tau_0, &normalize_i) in
1146            izip!(torque_n1.iter(), torque_0.iter(), normalize.iter())
1147        {
1148            objective += normalize_i * (tau_n1 - tau_0).abs();
1149        }
1150    }
1151    if num_stationary.1 > 0 {
1152        // |tau[i][n-num_stationary.1] - tau[i][n]| * normalize[i]
1153        let torque_n2 = torque.column(n - num_stationary.1);
1154        let torque_n = torque.column(n);
1155        for (tau_n2, tau_n, &normalize_i) in
1156            izip!(torque_n2.iter(), torque_n.iter(), normalize.iter())
1157        {
1158            objective += normalize_i * (tau_n2 - tau_n).abs();
1159        }
1160    }
1161    // Consider (tau[i][k+1] - tau[i][k]) * normalize[i] for k in num_stationary.0..(n-num_stationary.1)
1162    for (torque_col_curr, torque_col_next) in torque
1163        .column_iter()
1164        .tuple_windows()
1165        .skip(num_stationary.0)
1166        .take(n - num_stationary.0 - num_stationary.1)
1167    {
1168        for (tau_k, tau_k_next, &normalize_i) in izip!(
1169            torque_col_curr.iter(),
1170            torque_col_next.iter(),
1171            normalize.iter()
1172        ) {
1173            objective += normalize_i * (tau_k_next - tau_k).abs();
1174        }
1175    }
1176
1177    objective
1178}
1179
1180/// Compute the objective value for Linear in COPP3 optimization.
1181#[inline(always)]
1182fn objective_value_linear_copp3(
1183    a_profile: &[f64],
1184    b_profile: &[f64],
1185    alpha: &[f64],
1186    beta: &[f64],
1187) -> f64 {
1188    // objective: minimize \sum (alpha[k]*a[k] + beta[k]*b[k])
1189    let mut objective = 0.0;
1190    for (a_curr, alpha_curr) in a_profile.iter().zip(alpha.iter()) {
1191        // alpha[k]*a[k]
1192        objective += a_curr * alpha_curr;
1193    }
1194    for (b_curr, beta_curr) in b_profile.iter().zip(beta.iter()) {
1195        // beta[k]*b[k]
1196        objective += b_curr * beta_curr;
1197    }
1198    objective
1199}
1200
1201/// Compute the objective value for COPP3 optimization.
1202pub(crate) fn objective_value_copp3_opt<M: RobotTorque>(
1203    problem: &Copp3Problem<M>,
1204    profile: Topp3ProfileRef<'_>,
1205) -> (f64, Vec<f64>) {
1206    let (a_profile, b_profile, num_stationary) = profile;
1207    let s = problem
1208        .robot
1209        .constraints
1210        .s_vec(problem.idx_s_start, problem.idx_s_start + a_profile.len());
1211    let Ok(s) = s else {
1212        return (f64::INFINITY, vec![0.0; problem.objectives.len()]);
1213    };
1214    if a_profile.len() != s.len() || b_profile.len() != s.len() {
1215        return (f64::INFINITY, vec![0.0; problem.objectives.len()]);
1216    }
1217    let (a_sqrt_down, weight_a_time) = if problem.objectives.iter().any(|obj| {
1218        matches!(
1219            obj,
1220            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _)
1221        )
1222    }) {
1223        (
1224            a_profile
1225                .iter()
1226                .map(|a| 1.0 / a.sqrt().max(1E-16))
1227                .collect(),
1228            get_weight_a_topp3(&s, num_stationary),
1229        )
1230    } else {
1231        (vec![], vec![])
1232    };
1233    let weight_a_torque = if problem
1234        .objectives
1235        .iter()
1236        .any(|obj| matches!(obj, CoppObjective::ThermalEnergy(_, _)))
1237    {
1238        get_weight_a_copp3(&s, num_stationary)
1239    } else {
1240        vec![]
1241    };
1242    let torque = if problem.objectives.iter().any(|obj| {
1243        matches!(
1244            obj,
1245            CoppObjective::ThermalEnergy(_, _) | CoppObjective::TotalVariationTorque(_, _)
1246        )
1247    }) {
1248        let torque_result =
1249            problem
1250                .robot
1251                .get_torque_with_ab(a_profile, b_profile, problem.idx_s_start);
1252        match torque_result {
1253            Ok(torque) => torque,
1254            _ => return (f64::INFINITY, vec![0.0; problem.objectives.len()]),
1255        }
1256    } else {
1257        DMatrix::<f64>::zeros(0, 0)
1258    };
1259    let mut obj_val = Vec::with_capacity(problem.objectives.len());
1260    let mut obj_val_total = 0.0;
1261    for obj in problem.objectives {
1262        match obj {
1263            CoppObjective::Time(weight) => {
1264                let obj_here =
1265                    objective_value_time_copp3(&a_sqrt_down, &weight_a_time, num_stationary);
1266                obj_val.push(obj_here);
1267                obj_val_total += weight * obj_here;
1268            }
1269            CoppObjective::ThermalEnergy(weight, normalize) => {
1270                let obj_here = objective_value_thermal_energy_copp3(
1271                    &a_sqrt_down,
1272                    &weight_a_torque,
1273                    num_stationary,
1274                    &torque,
1275                    normalize,
1276                );
1277                obj_val.push(obj_here);
1278                obj_val_total += weight * obj_here;
1279            }
1280            CoppObjective::TotalVariationTorque(weight, normalize) => {
1281                let obj_here = objective_value_tv_torque_copp3(&torque, normalize, num_stationary);
1282                obj_val.push(obj_here);
1283                obj_val_total += weight * obj_here;
1284            }
1285            CoppObjective::Linear(weight, alpha, beta) => {
1286                let obj_here = objective_value_linear_copp3(a_profile, b_profile, alpha, beta);
1287                obj_val.push(obj_here);
1288                obj_val_total += weight * obj_here;
1289            }
1290        }
1291    }
1292    (obj_val_total, obj_val)
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297    use super::*;
1298    use crate::copp::copp2::stable::basic::{Topp2ProblemBuilder, s_to_t_topp2};
1299    use crate::copp::copp2::stable::reach_set2::ReachSet2OptionsBuilder;
1300    use crate::copp::copp2::stable::topp2_ra::topp2_ra;
1301    use crate::copp::copp3::stable::basic::{Copp3ProblemBuilder, s_to_t_topp3};
1302    use crate::copp::copp3::stable::topp3_lp::topp3_lp;
1303    use crate::copp::copp3::stable::topp3_socp::topp3_socp;
1304    use crate::copp::{ClarabelOptionsBuilder, default_clarabel_settings};
1305    use crate::path::{add_symmetric_axial_limits_for_test, lissajous_path_for_test};
1306    use crate::robot::robot_core::Robot;
1307    use std::time::Instant;
1308    use std::vec;
1309
1310    #[test]
1311    fn test_copp3_lp() -> Result<(), CoppError> {
1312        run_test_copp3_lp_repeated(1, false)
1313    }
1314
1315    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
1316    /// Average 100 experiments: tc_ra = 0.3270 ms, tc_lp = 237.4646 ms, tc_copp = 232.0414 ms, tf_ra = 6.304760, tf_lp = 6.524883, tf_copp = 6.524883, obj_lp = -0.031621, obj_copp = -0.031621
1317    #[test]
1318    #[ignore = "slow"]
1319    fn test_copp3_lp_robust() -> Result<(), CoppError> {
1320        run_test_copp3_lp_repeated(100, true)
1321    }
1322
1323    #[test]
1324    fn test_copp3_qp() -> Result<(), CoppError> {
1325        run_test_copp3_qp_repeated(1, false)
1326    }
1327
1328    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
1329    /// Average 100 experiments (fail 0): tc_ra = 0.3452 ms, tc_qp = 329.8604 ms, tc_copp = 302.8725 ms, tf_ra = 6.122942, tf_qp = 6.342830, tf_copp = 6.342830, obj_qp = 6.348896, obj_copp = 6.348896
1330    #[test]
1331    #[ignore = "slow"]
1332    fn test_copp3_qp_robust() -> Result<(), CoppError> {
1333        run_test_copp3_qp_repeated(100, true)
1334    }
1335
1336    #[test]
1337    fn test_all_objectives() -> Result<(), CoppError> {
1338        run_test_all_objectives_repeated(1, false)
1339    }
1340
1341    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
1342    /// Average 100 experiments (fail 0):
1343    /// Case 0: tc=301.449ms, obj=[6.355272621082504, 37.88484670012183, 30.538889633761094, 0.0014588588234334063]
1344    /// Case 1: tc=345.585ms, obj=[8.929951413267652, 11.232320784989641, 15.611002914308818, 0.001319403151583034]
1345    /// Case 2: tc=303.617ms, obj=[15.7679747735929, 2.0044043744079847, 5.695644764673545, 0.0010702913648672737]
1346    /// Case 3: tc=440.100ms, obj=[11.999657677994689, 5.6638102612967565, 5.999807538045939, 0.00022405699762245119]
1347    /// Case 4: tc=306.763ms, obj=[6.355272615415654, 37.884856482672014, 30.621301350094082, 0.0014588597553749254]
1348    #[test]
1349    #[ignore = "slow"]
1350    fn test_all_objectives_robust() -> Result<(), CoppError> {
1351        run_test_all_objectives_repeated(100, true)
1352    }
1353
1354    fn run_test_copp3_lp_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
1355        let mut tc_sum_ra = 0.0;
1356        let mut tc_sum_lp = 0.0;
1357        let mut tc_sum_copp = 0.0;
1358        let mut tf_sum_ra = 0.0;
1359        let mut tf_sum_lp = 0.0;
1360        let mut tf_sum_copp = 0.0;
1361        let mut obj_sum_lp = 0.0;
1362        let mut obj_sum_copp = 0.0;
1363
1364        for i_exp in 0..n_exp {
1365            let n: usize = 1000;
1366            let dim = 7;
1367            let mut robot = Robot::with_capacity(dim, n);
1368
1369            let mut rng = rand::rng();
1370            let (s, path, omega, phi) =
1371                lissajous_path_for_test(dim, n, &mut rng).expect("random range is valid");
1372            robot
1373                .with_s(&s.as_view())?
1374                .with_q_from_path_3rd(&path, 0, n)?;
1375            add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, Some(5.0))?;
1376
1377            let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
1378            // Step 1. Topp2-RA
1379            let start = Instant::now();
1380            let options_ra0 = ReachSet2OptionsBuilder::new()
1381                .lp_feas_tol(1E-9)
1382                .a_cmp_abs_tol(1E-9)
1383                .a_cmp_rel_tol(1E-9)
1384                .build()?;
1385            let a_ra0 = topp2_ra(&topp2_problem, &options_ra0)?;
1386            let tc_ra0 = start.elapsed().as_secs_f64() * 1E3;
1387            let (tf_ra0, _) = s_to_t_topp2(s.as_slice(), &a_ra0, 0.0)?;
1388
1389            let objectives = [CoppObjective::Linear(
1390                1.0,
1391                &get_weight_a_topp3(&s.as_slice()[0..n], (1, 1))
1392                    .iter()
1393                    .map(|&w_a| -w_a)
1394                    .collect_vec(),
1395                &vec![0.0; n],
1396            )];
1397            let copp3_problem = Copp3ProblemBuilder::new(
1398                &mut robot,
1399                &objectives,
1400                0,
1401                &a_ra0,
1402                (0.0, 0.0),
1403                (0.0, 0.0),
1404            )
1405            .build_with_linearization()?;
1406
1407            // Step 2. Test Copp3-SOCP
1408            let start = Instant::now();
1409            let profile_copp = {
1410                let mut settings = default_clarabel_settings();
1411                settings.tol_gap_rel = 1E-6;
1412                let options = ClarabelOptionsBuilder::with_clarabel_setting(settings)
1413                    .allow_almost_solved(true)
1414                    .build()?;
1415                let (result, solution) = copp3_socp_expert(&copp3_problem, &options)?;
1416                if let Some(result) = result {
1417                    result
1418                } else {
1419                    return Err(CoppError::ClarabelSolverStatus(
1420                        "copp3_socp".into(),
1421                        solution.status,
1422                    ));
1423                }
1424            };
1425            let tc_copp = start.elapsed().as_secs_f64() * 1E3;
1426            // Test time profile generation
1427            let (tf_copp, _) = s_to_t_topp3(s.as_slice(), profile_copp.as_parts(), 0.0)?;
1428
1429            // Step 3. Test Topp3-LP
1430            let options_lp = ClarabelOptionsBuilder::new()
1431                .allow_almost_solved(true)
1432                .build()?;
1433            let start = Instant::now();
1434            let profile_lp = topp3_lp(&copp3_problem.as_topp3_problem(), &options_lp)?;
1435            let tc_lp = start.elapsed().as_secs_f64() * 1E3;
1436            // Test time profile generation
1437            let (tf_lp, _) = s_to_t_topp3(s.as_slice(), profile_lp.as_parts(), 0.0)?;
1438
1439            let (obj_lp, _) = objective_value_copp3_opt(&copp3_problem, profile_lp.as_parts());
1440            let (obj_copp, _) = objective_value_copp3_opt(&copp3_problem, profile_copp.as_parts());
1441
1442            if flag_print_step {
1443                crate::verbosity_log!(
1444                    crate::diag::Verbosity::Summary,
1445                    "Exp #{}: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_copp = {:.4} ms, tf_ra = {:.6}, tf_lp = {:.6}, tf_copp = {:.6}, obj_lp = {:.6}, obj_copp = {:.6}",
1446                    i_exp + 1,
1447                    tc_ra0,
1448                    tc_lp,
1449                    tc_copp,
1450                    tf_ra0,
1451                    tf_lp,
1452                    tf_copp,
1453                    obj_lp,
1454                    obj_copp
1455                );
1456            }
1457
1458            if (tf_copp - tf_lp).abs() > 1e-8 {
1459                crate::verbosity_log!(
1460                    crate::diag::Verbosity::Summary,
1461                    "omega = {omega:?}\nphi = {phi:?}"
1462                );
1463                crate::verbosity_log!(
1464                    crate::diag::Verbosity::Debug,
1465                    "COPP3 time optimality failed! tf_copp - tf_lp = {}",
1466                    tf_copp - tf_lp
1467                );
1468            }
1469
1470            tc_sum_ra += tc_ra0;
1471            tc_sum_lp += tc_lp;
1472            tc_sum_copp += tc_copp;
1473            tf_sum_ra += tf_ra0;
1474            tf_sum_lp += tf_lp;
1475            tf_sum_copp += tf_copp;
1476            obj_sum_lp += obj_lp;
1477            obj_sum_copp += obj_copp;
1478        }
1479
1480        crate::verbosity_log!(
1481            crate::diag::Verbosity::Summary,
1482            "Average {} experiments: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_copp = {:.4} ms, tf_ra = {:.6}, tf_lp = {:.6}, tf_copp = {:.6}, obj_lp = {:.6}, obj_copp = {:.6}",
1483            n_exp,
1484            tc_sum_ra / n_exp as f64,
1485            tc_sum_lp / n_exp as f64,
1486            tc_sum_copp / n_exp as f64,
1487            tf_sum_ra / n_exp as f64,
1488            tf_sum_lp / n_exp as f64,
1489            tf_sum_copp / n_exp as f64,
1490            obj_sum_lp / n_exp as f64,
1491            obj_sum_copp / n_exp as f64
1492        );
1493
1494        Ok(())
1495    }
1496
1497    fn run_test_copp3_qp_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
1498        let mut tc_sum_ra = 0.0;
1499        let mut tc_sum_qp = 0.0;
1500        let mut tc_sum_copp = 0.0;
1501        let mut tf_sum_ra = 0.0;
1502        let mut tf_sum_qp = 0.0;
1503        let mut tf_sum_copp = 0.0;
1504        let mut obj_sum_qp = 0.0;
1505        let mut obj_sum_copp = 0.0;
1506        let mut succeed = 0;
1507
1508        for i_exp in 0..n_exp {
1509            let n: usize = 1000;
1510            let dim = 7;
1511            let mut robot = Robot::with_capacity(dim, n);
1512
1513            let mut rng = rand::rng();
1514            let (s, path, omega, phi) =
1515                lissajous_path_for_test(dim, n, &mut rng).expect("random range is valid");
1516            robot
1517                .with_s(&s.as_view())?
1518                .with_q_from_path_3rd(&path, 0, n)?;
1519            add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, Some(5.0))?;
1520
1521            let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
1522            // Step 1. Topp2-RA
1523            let start = Instant::now();
1524            let options_ra0 = ReachSet2OptionsBuilder::new()
1525                .lp_feas_tol(1E-9)
1526                .a_cmp_abs_tol(1E-9)
1527                .a_cmp_rel_tol(1E-9)
1528                .build()?;
1529            let a_ra0 = topp2_ra(&topp2_problem, &options_ra0)?;
1530            let tc_ra0 = start.elapsed().as_secs_f64() * 1E3;
1531            let (tf_ra0, _) = s_to_t_topp2(s.as_slice(), &a_ra0, 0.0)?;
1532
1533            let objective = [CoppObjective::Time(1.0)];
1534            let copp3_problem =
1535                Copp3ProblemBuilder::new(&mut robot, &objective, 0, &a_ra0, (0.0, 0.0), (0.0, 0.0))
1536                    .build_with_linearization()?;
1537
1538            // Step 2. Test Copp3-SOCP
1539            let start = Instant::now();
1540            let profile_copp = {
1541                let mut settings = default_clarabel_settings();
1542                settings.tol_gap_rel = 1E-6;
1543                let options = ClarabelOptionsBuilder::with_clarabel_setting(settings)
1544                    .allow_almost_solved(true)
1545                    .build()?;
1546                let (result, solution) = match copp3_socp_expert(&copp3_problem, &options) {
1547                    Ok(res) => res,
1548                    Err(_) => {
1549                        crate::verbosity_log!(
1550                            crate::diag::Verbosity::Debug,
1551                            "Exp #{}: Clarabel solver failed in copp3_socp_expert!",
1552                            i_exp + 1
1553                        );
1554                        continue;
1555                    }
1556                };
1557                if let Some(result) = result {
1558                    result
1559                } else {
1560                    return Err(CoppError::ClarabelSolverStatus(
1561                        "copp3_socp".into(),
1562                        solution.status,
1563                    ));
1564                }
1565            };
1566            let tc_copp = start.elapsed().as_secs_f64() * 1E3;
1567            // Test time profile generation
1568            let (tf_copp, _) = s_to_t_topp3(s.as_slice(), profile_copp.as_parts(), 0.0)?;
1569
1570            // Step 3. Test Topp3-LP
1571            let start = Instant::now();
1572            let options_qp = ClarabelOptionsBuilder::new()
1573                .allow_almost_solved(true)
1574                .build()?;
1575            let profile_qp = topp3_socp(&copp3_problem.as_topp3_problem(), &options_qp)?;
1576            let tc_qp = start.elapsed().as_secs_f64() * 1E3;
1577            // Test time profile generation
1578            let (tf_qp, _) = s_to_t_topp3(s.as_slice(), profile_qp.as_parts(), 0.0)?;
1579
1580            let (obj_qp, _) = objective_value_copp3_opt(&copp3_problem, profile_qp.as_parts());
1581            let (obj_copp, _) = objective_value_copp3_opt(&copp3_problem, profile_copp.as_parts());
1582
1583            if flag_print_step {
1584                crate::verbosity_log!(
1585                    crate::diag::Verbosity::Summary,
1586                    "Exp #{}: tc_ra = {:.4} ms, tc_qp = {:.4} ms, tc_copp = {:.4} ms, tf_ra = {:.6}, tf_qp = {:.6}, tf_copp = {:.6}, obj_qp = {:.6}, obj_copp = {:.6}",
1587                    i_exp + 1,
1588                    tc_ra0,
1589                    tc_qp,
1590                    tc_copp,
1591                    tf_ra0,
1592                    tf_qp,
1593                    tf_copp,
1594                    obj_qp,
1595                    obj_copp
1596                );
1597            }
1598
1599            if (tf_copp - tf_qp).abs() > 1e-4 || (obj_copp - obj_qp).abs() > 1e-4 {
1600                crate::verbosity_log!(
1601                    crate::diag::Verbosity::Summary,
1602                    "omega = {omega:?}\nphi = {phi:?}"
1603                );
1604                crate::verbosity_log!(
1605                    crate::diag::Verbosity::Debug,
1606                    "COPP3 time optimality failed at Exp #{}! tf_copp - tf_qp = {}, obj_copp - obj_qp = {}",
1607                    i_exp + 1,
1608                    tf_copp - tf_qp,
1609                    obj_copp - obj_qp
1610                );
1611            }
1612
1613            tc_sum_ra += tc_ra0;
1614            tc_sum_qp += tc_qp;
1615            tc_sum_copp += tc_copp;
1616            tf_sum_ra += tf_ra0;
1617            tf_sum_qp += tf_qp;
1618            tf_sum_copp += tf_copp;
1619            obj_sum_qp += obj_qp;
1620            obj_sum_copp += obj_copp;
1621            succeed += 1;
1622        }
1623
1624        crate::verbosity_log!(
1625            crate::diag::Verbosity::Summary,
1626            "Average {n_exp} experiments (fail {}): tc_ra = {:.4} ms, tc_qp = {:.4} ms, tc_copp = {:.4} ms, tf_ra = {:.6}, tf_qp = {:.6}, tf_copp = {:.6}, obj_qp = {:.6}, obj_copp = {:.6}",
1627            n_exp - succeed,
1628            tc_sum_ra / succeed as f64,
1629            tc_sum_qp / succeed as f64,
1630            tc_sum_copp / succeed as f64,
1631            tf_sum_ra / succeed as f64,
1632            tf_sum_qp / succeed as f64,
1633            tf_sum_copp / succeed as f64,
1634            obj_sum_qp / succeed as f64,
1635            obj_sum_copp / succeed as f64
1636        );
1637
1638        Ok(())
1639    }
1640
1641    fn run_test_all_objectives_repeated(
1642        n_exp: usize,
1643        flag_print_step: bool,
1644    ) -> Result<(), CoppError> {
1645        let mut tc_sum_case0 = 0.0;
1646        let mut tc_sum_case1 = 0.0;
1647        let mut tc_sum_case2 = 0.0;
1648        let mut tc_sum_case3 = 0.0;
1649        let mut tc_sum_case4 = 0.0;
1650        let mut obj_sum_case0 = vec![0.0; 4];
1651        let mut obj_sum_case1 = vec![0.0; 4];
1652        let mut obj_sum_case2 = vec![0.0; 4];
1653        let mut obj_sum_case3 = vec![0.0; 4];
1654        let mut obj_sum_case4 = vec![0.0; 4];
1655
1656        let mut succeed = 0;
1657
1658        for i_exp in 0..n_exp {
1659            let n: usize = 1000;
1660            let dim = 7;
1661            let mut robot = Robot::with_capacity(dim, n);
1662
1663            let mut rng = rand::rng();
1664            let (s, path, omega, phi) =
1665                lissajous_path_for_test(dim, n, &mut rng).expect("random range is valid");
1666
1667            if flag_print_step {
1668                crate::verbosity_log!(
1669                    crate::diag::Verbosity::Summary,
1670                    "omega = {omega:?}\nphi = {phi:?}"
1671                );
1672            }
1673            robot
1674                .with_s(&s.as_view())?
1675                .with_q_from_path_3rd(&path, 0, n)?;
1676            add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, Some(5.0))?;
1677
1678            let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
1679            // Step 1. Topp2-RA
1680            let options_ra0 = ReachSet2OptionsBuilder::new()
1681                .lp_feas_tol(1E-9)
1682                .a_cmp_abs_tol(1E-9)
1683                .a_cmp_rel_tol(1E-9)
1684                .build()?;
1685            let a_ra0 = topp2_ra(&topp2_problem, &options_ra0)?;
1686
1687            // Test different objectives in COPP2 optimization
1688            let objectives_test = [
1689                CoppObjective::Time(1.0),
1690                CoppObjective::ThermalEnergy(1.0, &vec![1.0; dim]),
1691                CoppObjective::TotalVariationTorque(1.0, &vec![1.0; dim]),
1692                CoppObjective::Linear(1.0, &vec![0.0; n], &vec![1.0; n - 1]),
1693            ];
1694
1695            // Case 0: Time only
1696            let obj_case0_src = [CoppObjective::Time(1.0)];
1697            let start = Instant::now();
1698            let (a_case0, b_case0, num_stationary) = {
1699                let copp3_problem = Copp3ProblemBuilder::new(
1700                    &mut robot,
1701                    &obj_case0_src,
1702                    0,
1703                    &a_ra0,
1704                    (0.0, 0.0),
1705                    (0.0, 0.0),
1706                )
1707                .build_with_linearization()?;
1708                let mut settings = default_clarabel_settings();
1709                settings.tol_gap_rel = 1E-6;
1710                let options = ClarabelOptionsBuilder::with_clarabel_setting(settings)
1711                    .allow_almost_solved(true)
1712                    .build()?;
1713                let (result, solution) = copp3_socp_expert(&copp3_problem, &options)?;
1714                if let Some(result) = result {
1715                    result.into_parts()
1716                } else {
1717                    crate::verbosity_log!(
1718                        crate::diag::Verbosity::Debug,
1719                        "{:?}",
1720                        CoppError::ClarabelSolverStatus(
1721                            "copp3_socp (case 0)".into(),
1722                            solution.status,
1723                        )
1724                    );
1725                    continue;
1726                }
1727            };
1728            let tc_copp3_case0 = start.elapsed().as_secs_f64() * 1E3;
1729            let (_, obj_case0) = {
1730                let copp3_problem = Copp3ProblemBuilder::new(
1731                    &mut robot,
1732                    &objectives_test,
1733                    0,
1734                    &a_ra0,
1735                    (0.0, 0.0),
1736                    (0.0, 0.0),
1737                )
1738                .build_with_linearization()?;
1739                objective_value_copp3_opt(&copp3_problem, (&a_case0, &b_case0, num_stationary))
1740            };
1741
1742            // Case 1: Time and ThermalEnergy
1743            let obj_case1 = [
1744                CoppObjective::Time(1.0),
1745                CoppObjective::ThermalEnergy(1.0, &vec![1.0; dim]),
1746            ];
1747            let start = Instant::now();
1748            let (a_case1, b_case1, num_stationary) = {
1749                let copp3_problem = Copp3ProblemBuilder::new(
1750                    &mut robot,
1751                    &obj_case1,
1752                    0,
1753                    &a_ra0,
1754                    (0.0, 0.0),
1755                    (0.0, 0.0),
1756                )
1757                .build_with_linearization()?;
1758                let mut settings = default_clarabel_settings();
1759                settings.tol_gap_rel = 1E-6;
1760                let options = ClarabelOptionsBuilder::with_clarabel_setting(settings)
1761                    .allow_almost_solved(true)
1762                    .build()?;
1763                let (result, solution) = copp3_socp_expert(&copp3_problem, &options)?;
1764                if let Some(result) = result {
1765                    result.into_parts()
1766                } else {
1767                    crate::verbosity_log!(
1768                        crate::diag::Verbosity::Debug,
1769                        "{:?}",
1770                        CoppError::ClarabelSolverStatus(
1771                            "copp3_socp (case 1)".into(),
1772                            solution.status,
1773                        )
1774                    );
1775                    continue;
1776                }
1777            };
1778            let tc_copp3_case1 = start.elapsed().as_secs_f64() * 1E3;
1779            let (_, obj_case1) = {
1780                let copp3_problem = Copp3ProblemBuilder::new(
1781                    &mut robot,
1782                    &objectives_test,
1783                    0,
1784                    &a_ra0,
1785                    (0.0, 0.0),
1786                    (0.0, 0.0),
1787                )
1788                .build_with_linearization()?;
1789                objective_value_copp3_opt(&copp3_problem, (&a_case1, &b_case1, num_stationary))
1790            };
1791            if obj_case1[0] < obj_case0[0] - 1E-3 || obj_case1[1] - 1E-3 > obj_case0[1] {
1792                let (tf_case0, _) = s_to_t_topp2(s.as_slice(), &a_case0, 0.0)?;
1793                let (tf_case1, _) = s_to_t_topp2(s.as_slice(), &a_case1, 0.0)?;
1794                crate::verbosity_log!(
1795                    crate::diag::Verbosity::Summary,
1796                    "omega = {omega:?}\nphi = {phi:?}"
1797                );
1798                crate::verbosity_log!(
1799                    crate::diag::Verbosity::Summary,
1800                    "Case 0: obj_time = {}, obj_thermal_energy = {}, tf = {}",
1801                    obj_case0[0],
1802                    obj_case0[1],
1803                    tf_case0
1804                );
1805                crate::verbosity_log!(
1806                    crate::diag::Verbosity::Summary,
1807                    "Case 1: obj_time = {}, obj_thermal_energy = {}, tf = {}",
1808                    obj_case1[0],
1809                    obj_case1[1],
1810                    tf_case1
1811                );
1812                crate::verbosity_log!(
1813                    crate::diag::Verbosity::Summary,
1814                    "Interesting... Cases 0 and 1"
1815                );
1816            }
1817
1818            // Case 2: Time and More ThermalEnergy
1819            let obj_case2 = [
1820                CoppObjective::Time(1.0),
1821                CoppObjective::ThermalEnergy(10.0, &vec![1.0; dim]),
1822            ];
1823            let start = Instant::now();
1824            let (a_case2, b_case2, num_stationary) = {
1825                let copp3_problem = Copp3ProblemBuilder::new(
1826                    &mut robot,
1827                    &obj_case2,
1828                    0,
1829                    &a_ra0,
1830                    (0.0, 0.0),
1831                    (0.0, 0.0),
1832                )
1833                .build_with_linearization()?;
1834                let mut settings = default_clarabel_settings();
1835                settings.tol_gap_rel = 1E-6;
1836                let options = ClarabelOptionsBuilder::with_clarabel_setting(settings)
1837                    .allow_almost_solved(true)
1838                    .build()?;
1839                let (result, solution) = copp3_socp_expert(&copp3_problem, &options)?;
1840                if let Some(result) = result {
1841                    result.into_parts()
1842                } else {
1843                    crate::verbosity_log!(
1844                        crate::diag::Verbosity::Debug,
1845                        "{:?}",
1846                        CoppError::ClarabelSolverStatus(
1847                            "copp3_socp (case 2)".into(),
1848                            solution.status,
1849                        )
1850                    );
1851                    continue;
1852                }
1853            };
1854            let tc_copp3_case2 = start.elapsed().as_secs_f64() * 1E3;
1855            let (_, obj_case2) = {
1856                let copp3_problem = Copp3ProblemBuilder::new(
1857                    &mut robot,
1858                    &objectives_test,
1859                    0,
1860                    &a_ra0,
1861                    (0.0, 0.0),
1862                    (0.0, 0.0),
1863                )
1864                .build_with_linearization()?;
1865                objective_value_copp3_opt(&copp3_problem, (&a_case2, &b_case2, num_stationary))
1866            };
1867            if obj_case2[0] < obj_case1[0] - 1E-3 || obj_case2[1] - 1E-3 > obj_case1[1] {
1868                crate::verbosity_log!(
1869                    crate::diag::Verbosity::Summary,
1870                    "omega = {omega:?}\nphi = {phi:?}"
1871                );
1872                crate::verbosity_log!(
1873                    crate::diag::Verbosity::Summary,
1874                    "Case 1: obj_time = {}, obj_thermal_energy = {}",
1875                    obj_case1[0],
1876                    obj_case1[1]
1877                );
1878                crate::verbosity_log!(
1879                    crate::diag::Verbosity::Summary,
1880                    "Case 2: obj_time = {}, obj_thermal_energy = {}",
1881                    obj_case2[0],
1882                    obj_case2[1]
1883                );
1884                crate::verbosity_log!(
1885                    crate::diag::Verbosity::Summary,
1886                    "Interesting... Cases 1 and 2"
1887                );
1888            }
1889
1890            // Case 3: Time and TotalVariationTorque
1891            let obj_case3 = [
1892                CoppObjective::Time(1.0),
1893                CoppObjective::TotalVariationTorque(1.0, &vec![1.0; dim]),
1894            ];
1895            let start = Instant::now();
1896            let (a_case3, b_case3, num_stationary) = {
1897                let copp3_problem = Copp3ProblemBuilder::new(
1898                    &mut robot,
1899                    &obj_case3,
1900                    0,
1901                    &a_ra0,
1902                    (0.0, 0.0),
1903                    (0.0, 0.0),
1904                )
1905                .build_with_linearization()?;
1906                let mut settings = default_clarabel_settings();
1907                settings.tol_gap_rel = 1E-6;
1908                let options = ClarabelOptionsBuilder::with_clarabel_setting(settings)
1909                    .allow_almost_solved(true)
1910                    .build()?;
1911                let (result, solution) = copp3_socp_expert(&copp3_problem, &options)?;
1912                if let Some(result) = result {
1913                    result.into_parts()
1914                } else {
1915                    crate::verbosity_log!(
1916                        crate::diag::Verbosity::Debug,
1917                        "{:?}",
1918                        CoppError::ClarabelSolverStatus(
1919                            "copp3_socp (case 3)".into(),
1920                            solution.status,
1921                        )
1922                    );
1923                    continue;
1924                }
1925            };
1926            let tc_copp3_case3 = start.elapsed().as_secs_f64() * 1E3;
1927            let (_, obj_case3) = {
1928                let copp3_problem = Copp3ProblemBuilder::new(
1929                    &mut robot,
1930                    &objectives_test,
1931                    0,
1932                    &a_ra0,
1933                    (0.0, 0.0),
1934                    (0.0, 0.0),
1935                )
1936                .build_with_linearization()?;
1937                objective_value_copp3_opt(&copp3_problem, (&a_case3, &b_case3, num_stationary))
1938            };
1939            if obj_case3[1] < obj_case1[1] - 1E-3 || obj_case3[2] - 1E-3 > obj_case1[2] {
1940                crate::verbosity_log!(
1941                    crate::diag::Verbosity::Summary,
1942                    "omega = {omega:?}\nphi = {phi:?}"
1943                );
1944                crate::verbosity_log!(
1945                    crate::diag::Verbosity::Summary,
1946                    "Case 1: obj_time = {}, obj_thermal_energy = {}, obj_total_variation_torque = {}",
1947                    obj_case1[0],
1948                    obj_case1[1],
1949                    obj_case1[2]
1950                );
1951                crate::verbosity_log!(
1952                    crate::diag::Verbosity::Summary,
1953                    "Case 3: obj_time = {}, obj_thermal_energy = {}, obj_total_variation_torque = {}",
1954                    obj_case3[0],
1955                    obj_case3[1],
1956                    obj_case3[2]
1957                );
1958                crate::verbosity_log!(
1959                    crate::diag::Verbosity::Summary,
1960                    "Interesting... Cases 1 and 3"
1961                );
1962            }
1963
1964            // Case 4: Time and Linear
1965            let obj_case4 = [
1966                CoppObjective::Time(1.0),
1967                CoppObjective::Linear(1.0, &vec![0.0; n], &vec![1.0; n]),
1968            ];
1969            let start = Instant::now();
1970            let (a_case4, b_case4, num_stationary) = {
1971                let copp3_problem = Copp3ProblemBuilder::new(
1972                    &mut robot,
1973                    &obj_case4,
1974                    0,
1975                    &a_ra0,
1976                    (0.0, 0.0),
1977                    (0.0, 0.0),
1978                )
1979                .build_with_linearization()?;
1980                let mut settings = default_clarabel_settings();
1981                settings.tol_gap_rel = 1E-6;
1982                let options = ClarabelOptionsBuilder::with_clarabel_setting(settings)
1983                    .allow_almost_solved(true)
1984                    .build()?;
1985                let (result, solution) = copp3_socp_expert(&copp3_problem, &options)?;
1986                if let Some(result) = result {
1987                    result.into_parts()
1988                } else {
1989                    crate::verbosity_log!(
1990                        crate::diag::Verbosity::Debug,
1991                        "{:?}",
1992                        CoppError::ClarabelSolverStatus(
1993                            "copp3_socp (case 4)".into(),
1994                            solution.status,
1995                        )
1996                    );
1997                    continue;
1998                }
1999            };
2000            let tc_copp3_case4 = start.elapsed().as_secs_f64() * 1E3;
2001            let (_, obj_case4) = {
2002                let copp3_problem = Copp3ProblemBuilder::new(
2003                    &mut robot,
2004                    &objectives_test,
2005                    0,
2006                    &a_ra0,
2007                    (0.0, 0.0),
2008                    (0.0, 0.0),
2009                )
2010                .build_with_linearization()?;
2011                objective_value_copp3_opt(&copp3_problem, (&a_case4, &b_case4, num_stationary))
2012            };
2013            if obj_case4[1] < obj_case1[1] - 1E-3 || obj_case4[3] - 1E-3 > obj_case1[3] {
2014                crate::verbosity_log!(
2015                    crate::diag::Verbosity::Summary,
2016                    "omega = {omega:?}\nphi = {phi:?}"
2017                );
2018                crate::verbosity_log!(
2019                    crate::diag::Verbosity::Summary,
2020                    "Case 1: obj_time = {}, obj_thermal_energy = {}, obj_linear = {}",
2021                    obj_case1[0],
2022                    obj_case1[1],
2023                    obj_case1[3]
2024                );
2025                crate::verbosity_log!(
2026                    crate::diag::Verbosity::Summary,
2027                    "Case 4: obj_time = {}, obj_thermal_energy = {}, obj_linear = {}",
2028                    obj_case4[0],
2029                    obj_case4[1],
2030                    obj_case4[3]
2031                );
2032                crate::verbosity_log!(
2033                    crate::diag::Verbosity::Summary,
2034                    "Interesting... Cases 1 and 4"
2035                );
2036            }
2037            if obj_case4[2] < obj_case2[2] - 1E-3 || obj_case4[3] - 1E-3 > obj_case2[3] {
2038                crate::verbosity_log!(
2039                    crate::diag::Verbosity::Summary,
2040                    "omega = {omega:?}\nphi = {phi:?}"
2041                );
2042                crate::verbosity_log!(
2043                    crate::diag::Verbosity::Summary,
2044                    "Case 2: obj_time = {}, obj_total_variation_torque = {}, obj_linear = {}",
2045                    obj_case2[0],
2046                    obj_case2[2],
2047                    obj_case2[3]
2048                );
2049                crate::verbosity_log!(
2050                    crate::diag::Verbosity::Summary,
2051                    "Case 4: obj_time = {}, obj_total_variation_torque = {}, obj_linear = {}",
2052                    obj_case4[0],
2053                    obj_case4[2],
2054                    obj_case4[3]
2055                );
2056                crate::verbosity_log!(
2057                    crate::diag::Verbosity::Summary,
2058                    "Interesting... Cases 2 and 4"
2059                );
2060            }
2061
2062            succeed += 1;
2063
2064            if flag_print_step {
2065                crate::verbosity_log!(
2066                    crate::diag::Verbosity::Summary,
2067                    "Exp #{}:\n Case 0: tc={:.3}ms, obj={:?}\n Case 1: tc={:.3}ms, obj={:?}\n Case 2: tc={:.3}ms, obj={:?}\n Case 3: tc={:.3}ms, obj={:?}\n Case 4: tc={:.3}ms, obj={:?}",
2068                    i_exp + 1,
2069                    tc_copp3_case0,
2070                    obj_case0,
2071                    tc_copp3_case1,
2072                    obj_case1,
2073                    tc_copp3_case2,
2074                    obj_case2,
2075                    tc_copp3_case3,
2076                    obj_case3,
2077                    tc_copp3_case4,
2078                    obj_case4
2079                );
2080            }
2081
2082            tc_sum_case0 += tc_copp3_case0;
2083            tc_sum_case1 += tc_copp3_case1;
2084            tc_sum_case2 += tc_copp3_case2;
2085            tc_sum_case3 += tc_copp3_case3;
2086            tc_sum_case4 += tc_copp3_case4;
2087            for i in 0..obj_case0.len() {
2088                obj_sum_case0[i] += obj_case0[i];
2089                obj_sum_case1[i] += obj_case1[i];
2090                obj_sum_case2[i] += obj_case2[i];
2091                obj_sum_case3[i] += obj_case3[i];
2092                obj_sum_case4[i] += obj_case4[i];
2093            }
2094        }
2095
2096        for i in 0..4 {
2097            obj_sum_case0[i] /= n_exp as f64;
2098            obj_sum_case1[i] /= n_exp as f64;
2099            obj_sum_case2[i] /= n_exp as f64;
2100            obj_sum_case3[i] /= n_exp as f64;
2101            obj_sum_case4[i] /= n_exp as f64;
2102        }
2103
2104        crate::verbosity_log!(
2105            crate::diag::Verbosity::Summary,
2106            "Average {n_exp} experiments (fail {}):\n Case 0: tc={:.3}ms, obj={obj_sum_case0:?}\n Case 1: tc={:.3}ms, obj={obj_sum_case1:?}\n Case 2: tc={:.3}ms, obj={obj_sum_case2:?}\n Case 3: tc={:.3}ms, obj={obj_sum_case3:?}\n Case 4: tc={:.3}ms, obj={obj_sum_case4:?}",
2107            n_exp - succeed,
2108            tc_sum_case0 / succeed as f64,
2109            tc_sum_case1 / succeed as f64,
2110            tc_sum_case2 / succeed as f64,
2111            tc_sum_case3 / succeed as f64,
2112            tc_sum_case4 / succeed as f64
2113        );
2114
2115        Ok(())
2116    }
2117}